package com.frinder.base.db;

import com.frinder.base.global.BaseException;
import com.frinder.base.utils.StringUtils;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Condition;
import tk.mybatis.mapper.entity.Example;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.frinder.base.global.BaseException.Code.*;

/**
 * @author frinder
 * @date 2018/5/19
 * @className AbstractService
 * @package com.kiktech.swarm.common.base
 * @Description: 基于通用MyBatis Mapper插件的Service接口的实现
 */
@Slf4j
@Transactional(rollbackFor = {Exception.class, BaseException.class})
public abstract class AbstractService<T> implements IService<T> {

    private final String templateResultSql = "SELECT * FROM (%s) _tmp WHERE 1 = 1 %s %s";
    private final String templateCountSql = "SELECT COUNT(1) FROM (%s) _tmp WHERE 1 = 1 %s";

    @Autowired(required = false)
    protected Mapper<T> mapper;

    @Autowired
    protected MyJdbcService myJdbcService;

    /**
     * 当前泛型真实类型的Class
     */
    private Class<T> modelClass;

    public AbstractService() {
        ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();
        modelClass = (Class<T>) pt.getActualTypeArguments()[0];
    }

    @Override
    public T lockById(long id) {
        Condition queryCondition = new Condition(modelClass);
        queryCondition.setForUpdate(true);
        queryCondition.createCriteria().andEqualTo("id", id);
        return mapper.selectByCondition(queryCondition).stream().findFirst().orElse(null);
    }

    @Override
    public int save(T model) {
        try {
            Method createTime = modelClass.getMethod("setCreateTime", Date.class);
            createTime.setAccessible(true);
            createTime.invoke(model, new Date());
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        int result = mapper.insertSelective(model);
        if (result <= 0) {
            throw new BaseException(SAVE_FAIL);
        }
        return result;
    }

    @Override
    public void saveList(List<T> models) {
        models.stream().forEach(o -> save(o));
    }

    @Override
    public int save(List<T> models) {
        try {
            Method createTime = modelClass.getMethod("setCreateTime", Date.class);
            createTime.setAccessible(true);
            Method updateTime = modelClass.getMethod("setUpdateTime", Date.class);
            updateTime.setAccessible(true);
            Method isDelete = modelClass.getMethod("setIsDeleted", Boolean.class);
            models.stream().forEach(m -> {
                try {
                    createTime.invoke(m, new Date());
                    updateTime.invoke(m, new Date());
                    isDelete.invoke(m, false);
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            });
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        int result = mapper.insertList(models);
        if (result <= 0) {
            throw new BaseException(SAVE_FAIL);
        }
        return result;
    }

    @Override
    public int deleteById(Long id) {
        int result = mapper.deleteByPrimaryKey(id);
        if (result <= 0) {
            throw new BaseException(DELETE_FAIL);
        }
        return result;
    }

    @Override
    public int deleteByIds(String ids) {
        int result = mapper.deleteByIds(ids);
        if (result <= 0) {
            throw new BaseException(DELETE_FAIL);
        }
        return result;
    }

    @Override
    public int update(T model) {
        try {
            Method updateTime = modelClass.getMethod("setUpdateTime", Date.class);
            updateTime.setAccessible(true);
            updateTime.invoke(model, new Date());
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        int result = mapper.updateByPrimaryKeySelective(model);
        if (result <= 0) {
            throw new BaseException(UPDATE_FAIL);
        }
        return result;
    }

    @Override
    public T findById(Long id) {
        return mapper.selectByPrimaryKey(id);
    }

    @Override
    public T findBySid(String sid) {
        return findBy("sid", sid);
    }

    @Override
    public T findBy(String property, Object value) {
        try {
            T model = modelClass.newInstance();
            Field field = modelClass.getDeclaredField(property);
            field.setAccessible(true);
            field.set(model, value);
            return mapper.selectOne(model);
        } catch (Exception e) {
            throw new BaseException(MORE_THAN_ONE);
        }
    }

    @Override
    public List<T> findByModel(T model) {
        return mapper.select(model);
    }

    @Override
    public List<T> findByIds(String ids) {
        return mapper.selectByIds(ids);
    }

    @Override
    public List<T> findByList(String property, Object value) {
        try {
            T model = modelClass.newInstance();
            Field field = modelClass.getDeclaredField(property);
            field.setAccessible(true);
            field.set(model, value);
            return mapper.select(model);
        } catch (Exception e) {
            return Lists.newArrayList();
        }
    }

    @Override
    public int getCount(T model) {
        return mapper.selectCount(model);
    }

    @Override
    public List<T> findByCondition(Condition condition) {
        return mapper.selectByCondition(condition);
    }

    @Override
    public List<T> findAll() {
        return mapper.selectAll();
    }

    @Override
    public PageInfo page(PageWrapper<T> page) {
        PageHelper.startPage(page.getPage(), page.getSize());
        Condition condition = createCondition(page);
        List<T> list = mapper.selectByCondition(condition);
        PageInfo pageInfo = new PageInfo(list);
        return pageInfo;
    }

    /**
     * @param page
     * @return
     */
    private Condition createCondition(PageWrapper<T> page) {
        Condition condition = new Condition(modelClass);
        try {
            Example.Criteria criteria = condition.createCriteria();
            if (null != page.getCreateTimeFrom()) {
                criteria.andGreaterThanOrEqualTo("createTime", page.getCreateTimeFrom());
            }
            if (null != page.getCreateTimeTo()) {
                criteria.andLessThanOrEqualTo("createTime", page.getCreateTimeTo());
            }
            if (null != page.getUpdateTimeFrom()) {
                criteria.andGreaterThanOrEqualTo("updateTime", page.getUpdateTimeFrom());
            }
            if (null != page.getUpdateTimeTo()) {
                criteria.andLessThanOrEqualTo("updateTime", page.getUpdateTimeTo());
            }
            Object target = page.getTarget();
            if (null != target) {
                Map<Object, Object> fields = new BeanMap(target);
                String key;
                Object v;
                Map<String, PageWrapper.Between> betweenMap = page.getBetweenList().stream().collect(Collectors.toMap(PageWrapper.Between::getField, s -> s));
                for (Map.Entry<Object, Object> entry : fields.entrySet()) {
                    key = String.valueOf(entry.getKey());
                    if ("class".equalsIgnoreCase(key)) {
                        continue;
                    }
                    v = entry.getValue();
                    if (page.getEqualFields().contains(key)) {
                        if (null == v) {
                            continue;
                        }
                        criteria.andEqualTo(key, v);
                    } else if (page.getNotEqualFields().contains(key)) {
                        if (null == v) {
                            continue;
                        }
                        criteria.andNotEqualTo(key, v);
                    } else if (page.getInFields().containsKey(key)) {
                        criteria.andIn(key, page.getInFields().get(key));
                    } else if (page.getNotInFields().containsKey(key)) {
                        criteria.andNotIn(key, page.getNotInFields().get(key));
                    } else if (page.getGreaterThanFields().containsKey(key)) {
                        criteria.andGreaterThan(key, page.getGreaterThanFields().get(key));
                    } else if (page.getGreaterThanOrEqualFields().containsKey(key)) {
                        criteria.andGreaterThanOrEqualTo(key, page.getGreaterThanOrEqualFields().get(key));
                    } else if (page.getLessThanFields().containsKey(key)) {
                        criteria.andLessThan(key, page.getLessThanFields().get(key));
                    } else if (page.getLessThanOrEqualFields().containsKey(key)) {
                        criteria.andLessThanOrEqualTo(key, page.getLessThanOrEqualFields().get(key));
                    } else if (betweenMap.containsKey(key)) {
                        criteria.andBetween(key, betweenMap.get(key).getMinValue(), betweenMap.get(key).getMaxValue());
                    } else {
                        if (null == v) {
                            continue;
                        }
                        if (v instanceof String) {
                            if (!Strings.isNullOrEmpty(v.toString())) {
                                criteria.andLike(key, String.format("%s%%", v));
                            }
                        } else if (v instanceof Date) {
                        } else {
                            criteria.andEqualTo(key, v);
                        }
                    }
                }
            }
            page.getOrders().stream().forEach(order -> {
                switch (order.getStrategy()) {
                    case DESC:
                        condition.orderBy(order.getField()).desc();
                        break;
                    case ASC:
                        condition.orderBy(order.getField()).asc();
                        break;
                    default:
                        break;
                }
            });
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return condition;
    }

    /**
     * @param sql
     * @param page
     * @return
     */
    @Override
    public PageInfo page(String sql, Class resultClass, PageWrapper<?> page) {
        Map<String, Object> queryParams = Maps.newHashMap();
        String alias = "_tmp";
        String whereSql = createBaseSql(alias, page, queryParams);
        String countSql = String.format(templateCountSql, sql, whereSql);
        long rowCount = myJdbcService.queryOne(countSql, queryParams, Long.class);
        String resultSql = String.format(templateResultSql, sql, whereSql, orderLimitSql(alias, page));
        List<?> results;
        if (resultClass.equals(Map.class)) {
            results = myJdbcService.queryMapList(resultSql, queryParams);
        } else {
            results = myJdbcService.queryList(resultSql, resultClass, queryParams);
        }
        PageInfo pageInfo = new PageInfo(results);
        pageInfo.setTotal(rowCount);
        pageInfo.setPages((int) (rowCount / (long) page.getSize() + (long) (rowCount % (long) page.getSize() == 0L ? 0 : 1)));
        pageInfo.setPageNum(page.getPage());
        pageInfo.setPageSize(page.getSize());
        return pageInfo;
    }

    /**
     * 构建sql
     *
     * @param page
     * @return
     */
    private String createBaseSql(String alias, PageWrapper<?> page, Map<String, Object> queryParams) {
        StringBuilder sqlBuilder = new StringBuilder();
        try {
            if (null != page.getCreateTimeFrom()) {
                sqlBuilder.append(String.format(" AND %s.create_time >= (:createTimeFrom)", alias));
                queryParams.put("createTimeFrom", page.getCreateTimeFrom());
            }
            if (null != page.getCreateTimeTo()) {
                sqlBuilder.append(String.format(" AND %s.create_time <= (:createTimeTo)", alias));
                queryParams.put("createTimeTo", page.getCreateTimeTo());
            }
            if (null != page.getUpdateTimeFrom()) {
                sqlBuilder.append(String.format(" AND %s.update_time >= (:updateTimeFrom)", alias));
                queryParams.put("updateTimeFrom", page.getUpdateTimeFrom());
            }
            if (null != page.getUpdateTimeTo()) {
                sqlBuilder.append(String.format(" AND %s.update_time <= (:updateTimeTo)", alias));
                queryParams.put("updateTimeTo", page.getUpdateTimeTo());
            }
            Object target = page.getTarget();
            if (null != target) {
                Map<Object, Object> fields;
                if (target instanceof Map) {
                    fields = (Map) target;
                } else {
                    fields = new BeanMap(target);
                }
                String key;
                Object v;
                Map<String, PageWrapper.Between> betweenMap = page.getBetweenList().stream().collect(Collectors.toMap(PageWrapper.Between::getField, s -> s));
                for (Map.Entry<Object, Object> entry : fields.entrySet()) {
                    key = String.valueOf(entry.getKey());
                    if ("class".equalsIgnoreCase(key)) {
                        continue;
                    }
                    v = entry.getValue();
                    if (page.getEqualFields().contains(key)) {
                        if (null == v) {
                            continue;
                        }
                        sqlBuilder.append(String.format(" AND %s.%s = (:%s) ", alias, StringUtils.upper2Underline(key), key));
                        queryParams.put(key, v);
                    } else if (page.getNotEqualFields().contains(key)) {
                        if (null == v) {
                            continue;
                        }
                        sqlBuilder.append(String.format(" AND %s.%s <> (:%s) ", alias, StringUtils.upper2Underline(key), key));
                        queryParams.put(key, v);
                    } else if (page.getInFields().containsKey(key)) {
                        sqlBuilder.append(String.format(" AND %s.%s IN (:%s) ", alias, StringUtils.upper2Underline(key), key));
                        queryParams.put(key, page.getInFields().get(key));
                    } else if (page.getNotInFields().containsKey(key)) {
                        sqlBuilder.append(String.format(" AND %s.%s NOT IN (:%s) ", alias, StringUtils.upper2Underline(key), key));
                        queryParams.put(key, page.getNotInFields().get(key));
                    } else if (page.getGreaterThanFields().containsKey(key)) {
                        sqlBuilder.append(String.format(" AND %s.%s > (:%s) ", alias, StringUtils.upper2Underline(key), key));
                        queryParams.put(key, page.getGreaterThanFields().get(key));
                    } else if (page.getGreaterThanOrEqualFields().containsKey(key)) {
                        sqlBuilder.append(String.format(" AND %s.%s >= (:%s) ", alias, StringUtils.upper2Underline(key), key));
                        queryParams.put(key, page.getGreaterThanOrEqualFields().get(key));
                    } else if (page.getLessThanFields().containsKey(key)) {
                        sqlBuilder.append(String.format(" AND %s.%s < (:%s) ", alias, StringUtils.upper2Underline(key), key));
                        queryParams.put(key, page.getLessThanFields().get(key));
                    } else if (page.getLessThanOrEqualFields().containsKey(key)) {
                        sqlBuilder.append(String.format(" AND %s.%s <= (:%s) ", alias, StringUtils.upper2Underline(key), key));
                        queryParams.put(key, page.getLessThanOrEqualFields().get(key));
                    } else if (betweenMap.containsKey(key)) {
                        String min = String.format("%sMin", key);
                        String max = String.format("%sMax", key);
                        sqlBuilder.append(
                                String.format(" AND %s.%s BETWEEN (:%s) AND (:%s)",
                                        alias,
                                        StringUtils.upper2Underline(key),
                                        min,
                                        max
                                )
                        );
                        queryParams.put(min, betweenMap.get(key).getMinValue());
                        queryParams.put(max, betweenMap.get(key).getMaxValue());
                    } else {
                        if (null == v) {
                            continue;
                        }
                        if (v instanceof String) {
                            if (!Strings.isNullOrEmpty(v.toString())) {
                                sqlBuilder.append(String.format(" AND %s.%s LIKE (:%s '%%') ", alias, StringUtils.upper2Underline(key), key));
                                queryParams.put(key, v);
                            }
                        } else if (v instanceof Date) {
                        } else {
                            sqlBuilder.append(String.format(" AND %s.%s = (:%s) ", alias, StringUtils.upper2Underline(key), key));
                            queryParams.put(key, v);
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return sqlBuilder.toString();
    }

    /**
     * @param alias
     * @param page
     * @return
     */
    private String orderLimitSql(String alias, PageWrapper<?> page) {
        StringBuilder sqlBuilder = new StringBuilder("");
        sqlBuilder.append(" ORDER BY ");
        List<String> orderList = Lists.newArrayList();
        page.getOrders().stream().forEach(order ->
                orderList.add(
                        String.format("%s.%s %s",
                                alias,
                                StringUtils.upper2Underline(order.getField()),
                                order.getStrategy().name()
                        )
                )
        );
        sqlBuilder.append(Joiner.on(",").join(orderList));
        int start = page.getPage() > 0 ? (page.getPage() - 1) * page.getSize() : 0;
        sqlBuilder.append(String.format(" LIMIT %d, %d", start, page.getSize()));
        return sqlBuilder.toString();
    }

    /**
     * @Author frinder
     * @Description 根据条件查询，支持 in/like/equal等
     * @Date 2019/1/21 11:25
     * @Param [wrapper]
     * @Return java.util.List<T>
     */
    @Override
    public List<T> findByModelWrapper(ModelWrapper<T> wrapper) {
        PageHelper.startPage(1, wrapper.getSize());
        Condition condition = createCondition(wrapper);
        return mapper.selectByCondition(condition);
    }

    /**
     * @param wrapper
     * @return
     */
    private Condition createCondition(ModelWrapper<T> wrapper) {
        Condition condition = new Condition(modelClass);
        try {
            Object target = wrapper.getTarget();
            if (null != target) {
                Example.Criteria criteria = condition.createCriteria();
                Map<Object, Object> fields = new BeanMap(target);
                String key;
                Object v;
                for (Map.Entry<Object, Object> entry : fields.entrySet()) {
                    key = String.valueOf(entry.getKey());
                    if ("class".equalsIgnoreCase(key)) {
                        continue;
                    }
                    v = entry.getValue();
                    if (wrapper.getLikeFields().contains(key)) {
                        if (null == v) {
                            continue;
                        }
                        criteria.andLike(key, String.format("%s%%", v));
                    } else if (wrapper.getEqualFields().contains(key)) {
                        if (null == v) {
                            continue;
                        }
                        criteria.andEqualTo(key, v);
                    } else if (wrapper.getNotEqualFields().contains(key)) {
                        if (null == v) {
                            continue;
                        }
                        criteria.andNotEqualTo(key, v);
                    } else if (wrapper.getInFields().containsKey(key)) {
                        criteria.andIn(key, wrapper.getInFields().get(key));
                    } else if (wrapper.getNotInFields().containsKey(key)) {
                        criteria.andNotIn(key, wrapper.getNotInFields().get(key));
                    } else if (wrapper.getGreaterThanFields().containsKey(key)) {
                        criteria.andGreaterThan(key, wrapper.getGreaterThanFields().get(key));
                    } else if (wrapper.getGreaterThanOrEqualFields().containsKey(key)) {
                        criteria.andGreaterThanOrEqualTo(key, wrapper.getGreaterThanOrEqualFields().get(key));
                    } else if (wrapper.getLessThanFields().containsKey(key)) {
                        criteria.andLessThan(key, wrapper.getLessThanFields().get(key));
                    } else if (wrapper.getLessThanOrEqualFields().containsKey(key)) {
                        criteria.andLessThanOrEqualTo(key, wrapper.getLessThanOrEqualFields().get(key));
                    } else {
                        if (null == v) {
                            continue;
                        }
                        if (v instanceof String) {
                            if (!Strings.isNullOrEmpty(v.toString())) {
                                criteria.andLike(key, String.format("%s%%", v));
                            }
                        } else if (v instanceof Date) {
                        } else {
                            criteria.andEqualTo(key, v);
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return condition;
    }
}
